﻿using System;
using System.Configuration;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Security.Principal;
using System.ServiceModel;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using Ensynch;
using Ensynch.FIM;
using Ensynch.Security.Principal;
using Microsoft.ReportingServices.DataProcessing;
using Microsoft.ReportingServices.Interfaces;
using Microsoft.ResourceManagement.Client;
using Microsoft.ResourceManagement.Client.WsTransfer;
using Microsoft.ResourceManagement.ObjectModel;
using Microsoft.ResourceManagement.ObjectModel.ResourceTypes;

namespace FimDataProcessingExtension
{
    public class FimConnection : IDbConnectionExtension
    {
        // Create a logger for use in this class.
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(
                System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        #region Variables

        private String connectionString;
        private System.Data.ConnectionState m_state = System.Data.ConnectionState.Closed;
        private String localizedName = @"FIM Service Information";
        private String impersonate;
        private String password;
        private String fimServicePrincipalName;
        private String dataSource;
        private String initialCatalog;
        private String fimServiceUrl;
        private Int32 connectionTimeout = 15;
        private DefaultClient defaultClient;

        private Boolean connectionOpened = false;
        private WindowsIdentity windowsIdentity;
        private Boolean integratedSecurity = false;

        private String domainName;
        private String userName;
        private string _userLogin;
        private string userLogin
        {
            get
            {
                return _userLogin;
            }
            set
            {
                _userLogin = value;
                if (_userLogin.IndexOf('\\') >= 0)
                {
                    string[] tokens = _userLogin.Split(new char[] {'\\'}, StringSplitOptions.RemoveEmptyEntries);
                    userName = tokens[tokens.Length - 1];
                    if (tokens.Length > 1)
                    {
                        domainName = tokens[0];
                    }
                }
            }
        }

        #endregion

        #region Constructor
        /// <summary>
        /// Fim connection constructor
        /// </summary>
        public FimConnection()
        {
        }

        /// <summary>
        /// Simple constructor that takes a connection string.
        /// </summary>
        /// <param name="connectionString"></param>
        public FimConnection(String connectionString)
        {
            this.connectionString = connectionString;
            parseConnectionString();
        }

        /// <summary>
        /// Gets the Default Client class that was created for this connection.
        /// </summary>
        public DefaultClient Client { get { return this.defaultClient; } }

        /// <summary>
        /// Gets the Mex Client with which the underlying clients use to communicate with the service.
        /// </summary>
        public MexClient ClientMexClient
        {
            get
            {
                return this.defaultClient.MexClient;
            }
        }
 
        /// <summary>
        /// Parse connection string
        /// </summary>
        private void parseConnectionString()
        {
            log.Debug("[parseConnectionString] Connection string:\r\n" + this.connectionString);
            StringBuilder buffer = new StringBuilder();

            try
            {
                String[] tokens = this.connectionString.Split(new Char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (String token in tokens)
                {
                    String[] keywords = token.Split(new Char[] { '=' });
                    if (keywords.Length == 2)
                    {
                        switch (keywords[0].ToLower().Trim())
                        {
                            case "data source":
                                this.dataSource = keywords[1].Replace("\"", "");
                                if (!dataSource.EndsWith("/"))
                                    dataSource += "/";
                                break;
                            case "initial catalog":
                                this.initialCatalog = keywords[1].Replace("\"", "");
                                break;
                            case "integrated security":
                                String result = keywords[1].ToLower();
                                if (result == "true")
                                {
                                    this.integratedSecurity = true;
                                }
                                else if (result == "false")
                                    this.integratedSecurity = false;
                                else
                                    throw new Exception("Connection String value \"" + keywords[1] + "\" is not valid for integrated security");
                                break;
                            case "user":
                                this.userLogin = keywords[1].Replace("\"", "");
                                break;
                            case "password":
                                this.password = keywords[1].Replace("\"", "");
                                break;
                            case "fimserviceprincipalname":
                                this.fimServicePrincipalName = keywords[1].Replace("\"", "");
                                break;
                            case "domain":
                                this.domainName = keywords[1].Replace("\"", "");
                                break;
                            case "connect timeout":
                            case "connection timeout":
                                if (!int.TryParse(keywords[1], out this.connectionTimeout) || this.connectionTimeout < 0)
                                {
                                    throw new Exception("Connection String keyword: \"" + keywords[0] + "\" requires an integer value >= 0");
                                }
                                break;
                            default:
                                throw new Exception("Connection String keyword: \"" + token + "\" was not recognized");
                        }
                        buffer.AppendLine(string.Format("{0} = {1}", 
                            keywords[0].PadRight(25), keywords[1]));
                    }
                    else
                    {
                        throw new Exception("Connection String operative: \"" + token + "\" is not correctly formed.");
                    }
                }
            }
            finally
            {
                log.Debug("[parseConnectionString] result:\r\n" + buffer.ToString());
            }

            /// At this point, we've parsed Integrated Security from the connection string,
            /// but it hasn't been set by the framework yet. Do not allow a username/password
            /// with Integrated Security = true. We could support this by impersonating the
            /// user since we have credentials, but I think this is unnecessary complexity.
            if (this.integratedSecurity && !string.IsNullOrEmpty(userLogin))
            {
                throw new NotSupportedException("Username/password is not supported with Integrated Security = true");
            }

            //
            // Make sure we have all the tokens we need.
            //
            testKeywordValue("data source", dataSource);
            testKeywordValue("initial catalog", initialCatalog);
            testKeywordValue("fimServicePrincipalName", fimServicePrincipalName);

            //
            // Since we have everything we can create the Web Service URL
            //
            fimServiceUrl = dataSource + initialCatalog;
        }
        /// <summary>
        /// Validates that a specific keyword has a value
        /// </summary>
        /// <param name="keyword"></param>
        /// <param name="value"></param>
        private void testKeywordValue(string keyword, string value)
        {
            if (value == null || value.Length == 0)
            {
                throw new Exception("Missing required value in connection string for: \"" + keyword);
            }
        }

        #endregion

        #region IDbConnection members

        /// <summary>
        /// A public property which Gets or Sets the string used to open a database.
        ///
        /// "Data Source=[Base_URL];initial catalog=[WebServiceRoot];uid=[FIM User];pwd=[FIM Password];pun=[Principle user name];domain=[Domain Name];upn domain=[UPN Domain Name]"
        /// 
        /// </summary>
        string IDbConnection.ConnectionString
        {
            get
            {
                // Always return exactly what the user set.
                // Security-sensitive information may be removed.
                return this.connectionString;
            }
            set
            {
                this.connectionString = value;
                parseConnectionString();
            }
        }

        /// <summary>
        /// Gets the time to wait, while trying to establish a connection, before terminating the attempt and generating an error. 
        /// </summary>
        int IDbConnection.ConnectionTimeout
        {
            get
            {
                // Returns the connection time-out value set in the connection
                // string. Zero indicates an indefinite time-out period.
                return 0;
            }
        }

        /// <summary>
        /// Returns the state of the current connection
        /// </summary>
        public System.Data.ConnectionState State
        {
            get
            {
                return m_state;
            }
        }

        /// <summary>
        /// Begins a transaction. Not implemented as this is a read only data extension
        /// </summary>
        /// <returns></returns>
        IDbTransaction IDbConnection.BeginTransaction()
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Opens a database connection with the settings specified by the ConnectionString property of the provider-specific Connection object. 
        /// </summary>
        void IDbConnection.Open()
        {
            DateTime start = DateTime.Now;
            DateTime finishInstantiation;
            StringBuilder buffer;
            TimeSpan elapsed;

            FimTools.LogCurrentUser();

            /*
            * Open the database connection and set the ConnectionState
            * property.
            */
            if (!this.connectionOpened)
            {
                log.DebugFormat(
@"[IDbConnection.Open] Opening client connection
fimServiceUrl           = {0}
fimServicePrincipalName = {1}", fimServiceUrl, fimServicePrincipalName);
                
                if (this.integratedSecurity)
                {
                    ///Impersonation and Custom Data Processing Extensions 
                    ///
                    ///If your custom data processing extension connects to data sources using 
                    ///impersonation, you must use the Open method on either the IDbConnection 
                    ///orIDbConnectionExtension interfaces to make the request. Alternately, 
                    ///you can store the user identity object (System.Security.Principal.WindowsIdentity) 
                    ///and then reuse it in the other data processing extension APIs.
                    ///
                    ///In previous releases of Reporting Services, all custom data processing 
                    ///extensions were called under user impersonation. In this release, only 
                    ///the Open method will be called while impersonating the user. If you have 
                    ///an existing data processing extension that requires integrated security, 
                    ///you must modify your code to use the Openmethod or store the user identity object.
                    ///
                    ///Pasted from <http://msdn.microsoft.com/en-us/library/ms345229.aspx> 

                    this.defaultClient = new DefaultClient(fimServiceUrl, fimServicePrincipalName, null);
                    this.windowsIdentity = WindowsIdentity.GetCurrent();
                    log.Debug("[IDbConnection.Open] Using Windows integrated security.");
                }
                else
                {
                    log.DebugFormat(
@"[IDbConnection.Open] Using these credentials
domainName = {0}
userName   = {1}", domainName, userName);
                    NetworkCredential clientCredential = new NetworkCredential(userName, password, domainName);
                    this.defaultClient = new DefaultClient(fimServiceUrl, fimServicePrincipalName, clientCredential);
                }

                finishInstantiation = DateTime.Now;

                #region Check the schema.
                //foreach (XmlSchema schema in schemaSet.Schemas())
                //{
                //    using (StringWriter sw = new StringWriter())
                //    {
                //        schema.Write(sw);
                //        log.Debug("[IDbConnection.Open] Schema contents:\r\n\r\n" + sw.ToString());
                //    }
                //}
                #endregion

                buffer = new StringBuilder();
                buffer.AppendLine("[IDbConnection.Open] Successfully opened connection.");
                elapsed = finishInstantiation - start;
                buffer.AppendLine("Total elapsed time: " + elapsed.ToString());
                log.Debug(buffer.ToString());

                FimTools.LogCurrentUser();

                connectionOpened = true;
            }
        }
        /// <summary>
        /// Closes the connection and releases any open resources.
        /// </summary>
        void IDbConnection.Close()
        {
            /*
            * Close the database connection and set the ConnectionState
            * property. 
            */
            MethodBase sourceMethod = EnsynchTools.WhichMethodCalledMe();
            log.DebugFormat("[IDbConnection.Close] called from [{0}.{1}]",
                sourceMethod.ReflectedType, sourceMethod.Name);
            if (connectionOpened)
            {
                log.Debug("[IDbConnection.Close] Disposing defaultClient.");
                defaultClient.Dispose();
                defaultClient = null;
                connectionOpened = false;
            }
            if (windowsIdentity != null)
            {
                if (!integratedSecurity)
                {
                    log.Debug("[IDbConnection.Close] Undoing impersonation.");
                    // Stop impersonating the user.
                    UndoImpersonation();
                }
                windowsIdentity.Dispose();
            }
            connectionOpened = false;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        IDbCommand IDbConnection.CreateCommand()
        {
            // Return a new instance of a command object.
            return new FimCommand(this);
        }


        #endregion

        #region IExtension Members

        string IExtension.LocalizedName
        {
            get
            {
                // Always return exactly what the user set.
                // Security-sensitive information may be removed.
                return this.localizedName;
            }
        }

        void IExtension.SetConfiguration(string configuration)
        {
            // Used to retrieve configuration data from the config file
            //TODO: Implement the Set configuration method based on the requirements
        }

        #endregion

        #region IDbConnectionExtension members

        /****
        * REQUIRED METHODS / PROPERTIES FROM IDbConnectionExtension.
        ****/
        /*
        * For data sources that require credentials, these properties
        * add support for storing secure credentials while designing
        * reports with Report Designer. The Data Source dialog will
        * include support for the Integrated checkbox as well as
        * text boxes for username and password.
        */
        bool IDbConnectionExtension.IntegratedSecurity
        {
            get
            {
                MethodBase sourceMethod = EnsynchTools.WhichMethodCalledMe();
                log.DebugFormat("[get_IDbConnectionExtension.IntegratedSecurity] called from [{0}.{1}]\r\nResult = {2}",
                    sourceMethod.ReflectedType, sourceMethod.Name, integratedSecurity);
                return integratedSecurity;
            }
            set
            {
                integratedSecurity = value;
                log.Debug("[set_IDbConnectionExtension.IntegratedSecurity] Set integratedSecurity = " + integratedSecurity);
            }
        }

        string IDbConnectionExtension.UserName
        {
            set
            {
                userLogin = value;
                log.Debug("[IDbConnectionExtension.UserName] Set userLogin = " + userLogin);
            }
        }

        string IDbConnectionExtension.Password
        {
            set
            {
                password = value;
            }
        }

        string IDbConnectionExtension.Impersonate
        {
            set
            {
                impersonate = value;
                log.Debug("[IDbConnectionExtension.Impersonate] Set impersonate = " + impersonate);
            }
        }

        #endregion

        #region Dispose

        void IDisposable.Dispose()
        {
            // TODO: Dispose implementation
        }

        #endregion

        private WindowsImpersonationContext impersonationContext;
        internal void MaybeImpersonate()
        {
            #if TRACE
            MethodBase src = EnsynchTools.WhichMethodCalledMe();
            log.DebugFormat("[{0}.{1}] MaybeImpersonate() called",
                src.ReflectedType.FullName, src.Name);
            #endif

            if (!this.integratedSecurity)
            {
                #if TRACE
                log.Debug("[MaybeImpersonate] integratedSecurity = false; nothing to do");
                #endif
                return;
            }

            if (impersonationContext != null)
            {
                #if TRACE
                log.Debug("[MaybeImpersonate] impersonationContext is already non-null: " + this.windowsIdentity.Name);
                #endif
                return;
            }

            WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
            if (this.windowsIdentity.User == currentUser.User)
            {
                #if TRACE
                log.Debug("[MaybeImpersonate] windowsIdentity.User == currentUser.User; nothing to do");
                #endif
                return;
            }

            MethodBase sourceMethod = EnsynchTools.WhichMethodCalledMe();
            log.DebugFormat("[{0}.{1}] Impersonating user: {2}",
                sourceMethod.ReflectedType.FullName, sourceMethod.Name, this.windowsIdentity.Name);
            impersonationContext = this.windowsIdentity.Impersonate();
            FimTools.LogCurrentUser();
        }

        internal void UndoImpersonation()
        {
            if (impersonationContext != null)
            {
                impersonationContext.Undo();
                impersonationContext.Dispose();
                impersonationContext = null;
            }
        }

        internal WindowsIdentity ConnectionUser
        {
            get
            {
                // windowsIdentity is valid only during open connection
                if (!this.connectionOpened)
                {
                    throw new InvalidOperationException("ConnectionUser is valid only during open connection.");
                    //When the connection is not opened, throw an exception
                }
                return this.windowsIdentity;
            }
        }

    }
}
